訪問者模式允許在不修改物件類別的情況下,對物件集合中的元素施加新的操作。
麥當勞的餐點有很多種點法,比如說一個麥香雞漢堡,可以單點、搭配薯條和可樂做成套餐,或是與其他餐點組成 1+1 的優惠組合。消費者的訂單就像各種餐點組合的集合,而金額的計算系統則如同訪問者,必須根據不同組合套用不同的計價規則,藉此得出正確的金額。
麥當勞的餐點有這麼多種組合方式,每一種組合的計價規則與明細的格式都不同。我們可以使用訪問者模式,將相同功能的操作集中在同一個訪問者中,統一操作介面,使程式碼更容易管理。
OrderItem
代表一個訂單項目,並提供一個接收訪問者的方法。
interface OrderItem {
accept(visitor: OrderItemVisitor): void;
}
以下是菜單項目、套餐和 1+1 優惠組合的具體類別,這三種類別各自擁有不同的資料,但它們都實現了 accept
方法,可以透過訪問者來進行操作。
class MenuItem implements OrderItem {
constructor(public name: string, public price: number) {}
accept(visitor: OrderItemVisitor): void {
visitor.visitMenuItem(this);
}
}
class Meal implements OrderItem {
constructor(
public main: MenuItem,
public side: MenuItem,
public drink: MenuItem
) {}
accept(visitor: OrderItemVisitor): void {
visitor.visitMeal(this);
}
}
class Combo implements OrderItem {
constructor(public left: MenuItem, public right: MenuItem) {}
accept(visitor: OrderItemVisitor): void {
visitor.visitCombo(this);
}
}
這是訂單項目的抽象訪問者,定義了各項具體訂單項目的訪問方法。
interface OrderItemVisitor {
visitMenuItem(menuItem: MenuItem): void;
visitMeal(meal: Meal): void;
visitCombo(combo: Combo): void;
}
PricingVisitor
是一個具體訪問者,用於計算訂單總價。它定義了各項具體訂單項目的計算邏輯,可以根據不同餐點組合來累積價格。
class PricingVisitor implements OrderItemVisitor {
private totalPrice: number = 0;
visitMenuItem(menuItem: MenuItem) {
this.totalPrice += menuItem.price;
}
visitMeal(meal: Meal) {
this.totalPrice += meal.main.price;
this.totalPrice += meal.side.price;
this.totalPrice += meal.drink.price;
}
visitCombo(combo: Combo) {
this.totalPrice += combo.left.price;
this.totalPrice += combo.right.price;
}
getTotalPrice() {
return this.totalPrice;
}
}
OrderDetailVisitor
是另一個具體的訪問者類別,用於生成訂單明細。它會根據不同餐點組合生成對應格式的訂單細項。
class OrderDetailVisitor implements OrderItemVisitor {
private details: string[] = [];
visitMenuItem(item: MenuItem) {
this.details.push(`- ${item.name}: $${item.price}`);
}
visitMeal(meal: Meal) {
this.details.push("- Meal:");
this.details.push(` - ${meal.main.name}: $${meal.main.price}`);
this.details.push(` - ${meal.side.name}: $${meal.side.price}`);
this.details.push(` - ${meal.drink.name}: $${meal.drink.price}`);
}
visitCombo(combo: Combo) {
this.details.push("- 1+1 Combo:");
this.details.push(` - ${combo.left.name}: $${combo.left.price}`);
this.details.push(` - ${combo.right.name}: $${combo.right.price}`);
}
getOrderDetails() {
return this.details.join("\n");
}
}
Order
類別代表一個訂單,擁有一個訂單項目清單,並透過 PricingVisitor
和 OrderDetailVisitor
來計算總價和產生訂單明細。
class Order {
private items: OrderItem[];
constructor(items: OrderItem[] = []) {
this.items = items;
}
calculateTotal() {
const visitor = new PricingVisitor();
this.items.forEach((item) => item.accept(visitor));
return visitor.getTotalPrice();
}
getOrderDetails() {
const visitor = new OrderDetailVisitor();
this.items.forEach((item) => item.accept(visitor));
return visitor.getOrderDetails();
}
}
建立一些餐點和套餐,並建立兩筆訂單,再印出兩筆訂單的總金額與商品明細。
class OrderTestDrive {
static main() {
const mcChicken = new MenuItem("McChicken", 50);
const filetOFish = new MenuItem("Filet-O-Fish", 45);
const fries = new MenuItem("Fries", 30);
const coke = new MenuItem("Coke", 25);
const lemonTea = new MenuItem("Lemon Tea", 20);
const iceCream = new MenuItem("Ice Cream", 35);
const meal = new Meal(mcChicken, fries, coke);
const combo = new Combo(fries, lemonTea);
const order1 = new Order([filetOFish, meal, combo]);
const order2 = new Order([filetOFish, iceCream]);
console.log(`Total for Order1: $${order1.calculateTotal()}`);
console.log(`Total for Order2: $${order2.calculateTotal()}`);
console.log("\nOrder1 Details:");
console.log(order1.getOrderDetails());
console.log("\nOrder2 Details:");
console.log(order2.getOrderDetails());
}
}
OrderTestDrive.main();
執行結果:
Total for Order1: $200
Total for Order2: $80
Order1 Details:
- Filet-O-Fish: $45
- Meal:
- McChicken: $50
- Fries: $30
- Coke: $25
- 1+1 Combo:
- Fries: $30
- Lemon Tea: $20
Order2 Details:
- Filet-O-Fish: $45
- Ice Cream: $35
訪問者模式可以處理包含不同介面物件的集合,並根據每個物件的具體類別執行不同的操作。這種模式將操作與物件類別分離,使我們能在不修改物件類別的前提下新增功能。當需要新增功能時,只需定義新的訪問者來處理各類物件的行為,無需更改物件類別的原始程式碼。這不僅讓程式邏輯更加集中,也能通過替換訪問者輕鬆實現新的邏輯。